package minov.parser;

/**
 * <h1>Argument parser</h1> Purpose of this library is to simplify process with
 * argument
 * parsing. Whole parsing is managed by class {@link ArgumentParser} that
 * collects parsing options and parses input arguments according to options.
 * </p>
 * <p>
 * To define parsing arguments are used classes inherited from
 * {@link ArgumentObject}. So to parse text argument, use {@link TextArgumet}
 * class. For defining your own types, you can simply create a new class
 * inherited from some existing class, that inherits from {@link ArgumentObject}
 * .
 * </p>
 * <p>
 * For adding argument use method {@link ArgumentParser.addArgument()}
 * </p>
 * 
 * 
 * <h3>Example</h3>
 * <pre>
 *      SwitchArgument version = new SwitchArgument("v", "version");
 *      parser.addArgument("version", version);
 *
 * </pre>
 * \include SimpleExample.java
 * prints
 * \include SimpleExampleResult.txt
 * 
 * <p>
 * You can also use unnamed argument. To set this argument, create argument with
 * null names
 * 
 * 
 * <h3>Example</h3>
 * <pre>
 *    test2FileArg = new FileArgument(null, null);
 *    test2ArgPars.addArgument("files", test2FileArg);
 *
 * </pre>
 *
 * </p>
 * 
 * 
 * <p>
 * For retrieving the parsed resultsuse the same ArgumentObject that was used for argument registration
 * <h3>Example</h3>
 * <pre>
 *     if (test2FileListArg.isEntered()) {
 *       String fileListName = test2FileListArg.getValue();
 *
 *        System.out.println("Filelist file: " + fileListName);
 *     } else {
 *        System.out.println("Filelist file was not specified");
 *     }
 * </pre>
 * </p>
 * 
 * <p>
 * For setting the minimum and maximum count of parameters of the argument use the {@link ArgumentObject.setValueCountRange()} function. 
 * You can use the FileArgument.UNLIMITED_ARGUMENT constant for unlimited parameters. 
 * null names
 * 
 * 
 * <h3>Example</h3>
 * <pre>
 *    test2FileArg = new FileArgument(null, null);
 *    test2FileArg.setValueCountRange(0, FileArgument.UNLIMITED_ARGUMENT);
 *    test2ArgPars.addArgument("files", test2FileArg);
 *
 * </pre>
 * </p>
 * 
 * 
 * @author Jakub Michalko
 * @author Jiří Novák
 * @mainpage Argument Parser
 */

import java.util.Deque;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Queue;

import minov.parser.utils.ArgumentMapper;
import minov.parser.utils.StringUtils;

/**
 * Main class that parses arguments. Class is working in two phases. First is
 * setting arguments and second parsing arguments.
 * 
 * <h3>Example</h3>
 * 
 * \include SimpleExample.java
 * prints
 * \include SimpleExampleResult.txt
 * 
 * @class ArgumentParser
 */
public class ArgumentParser {

   /**
    * Default constructor
    */
   public ArgumentParser() {
   }

   // ------------------------------------------
   // Parsing definition
   // ------------------------------------------

   /**
    * Register of all defined arguments
    * <id,Argument>
    */
   private ArgumentMapper _allDefinedArguments = new ArgumentMapper();

   /**
    * 
    * @return array with all registered arguments
    */
   public ArgumentObject[] getAllDefinedArguments() {
      return _allDefinedArguments.getAllArguments();

   }


   /**
    * Method for registering the arguments   
    * @param [in] id id/name of the argument
    * @param [in] newArgument properly configured ArgumentObject
    * @return registered ArgumentObject
    */
   public ArgumentObject addArgument(String id, ArgumentObject newArgument) {
      _allDefinedArguments.put(id, newArgument);

      newArgument.setOwnerParser(this);
      return newArgument;
   }

   /**
    * Register all argument aliases
    * 
    * @param argument
    *           input argument
    * @throws ArgumentDefinitionException
    *            if two argument has the same alias
    */
   private void registerArgumentAliases(ArgumentObject argument)
         throws ArgumentDefinitionException {
      String[] argumentAliases = argument.getArgumentNames();

      if (argumentAliases.length > 0) {
         for (String argumentName : argumentAliases) {
            if (!StringUtils.isEmptyString(argumentName)) {
               registerArgumentName(argumentName, argument);
            }
         }
      } else {
         registerNoNameArgument(argument);
      }

   }


   /**
    * Function for getting the id from the ArgumentObject
    * @param [in] argument ArgumentObject to get the id
    * @return the argument id
    */

   public String getArgumentId(ArgumentObject argument) {

      if (_allDefinedArguments.containsValue(argument)) {
         return _allDefinedArguments.getKey(argument);
      }
      return "";
   }

   // ------------------------------------------
   // Parsing initialization
   // ------------------------------------------

   /**
    * Map of full names of arguments
    * <shortName,argument>
    */
   private HashMap<String, ArgumentObject> _fullNamesTable =
         new HashMap<String, ArgumentObject>();

   /**
    * Map of short names
    * <shortName,argument>
    */
   private HashMap<Character, ArgumentObject> _shortNamesTable =
         new HashMap<Character, ArgumentObject>();

   /**
    * Order of all entered and parsed arguments
    */
   private Queue<ArgumentObject> _enteredArguments =
         new LinkedList<ArgumentObject>();

   /**
    * Queue of unnamed arguments, that remains in queue.
    */
   private Queue<ArgumentObject> _remainedNoNameArguments =
         new LinkedList<ArgumentObject>();

   /**
    * Initialization - refreshing all collections used for parsing
    */
   private void inintializeRegisters() {
      _shortNamesTable.clear();
      _fullNamesTable.clear();
      _enteredArguments.clear();
      _remainedNoNameArguments.clear();
   }

   /**
    * Pasing initialization.
    * Clear all collection used at parsing
    */
   private void initializeParsing() throws ArgumentDefinitionException {

      inintializeRegisters();

      for (ArgumentObject definedArgument : _allDefinedArguments
            .getAllArguments()) {
         registerArgumentAliases(definedArgument);
         definedArgument.reset();
      }

   }

   // ------------------------------------------
   // static functions
   // ------------------------------------------

   /**
    * Return true, if name is short name
    */
   private static boolean isShortName(String argumentName) {

      if (argumentName != null && argumentName.length() == 1)
         return true;
      return false;
   }

   // ------------------------------------------
   // Parsing initialization - registration
   // ------------------------------------------


   /**
    * Register argument under it's alias.
    * If argument has no alias (name), it will be stored as unmamed argument
    * (vaue).
    * @param argumentAlias argument alias
    * @param argument arguemnt intance
    * @throws ArgumentDefinitionException if thow arguments has the same alias
    */

   public void registerArgumentName(String argumentAlias,
         ArgumentObject argument) throws ArgumentDefinitionException {

      if (StringUtils.isEmptyString(argumentAlias))
         return;

      if (isShortName(argumentAlias)) {

         Character shortName = argumentAlias.charAt(0);
         if (!_shortNamesTable.containsKey(shortName)) {
            _shortNamesTable.put(shortName, argument);
         } else {
            if (_shortNamesTable.get(shortName) != argument)
               throw new ArgumentDefinitionException(
                     "Registration conflict: Option \"" + shortName
                           + "\" is defined twice.");
         }

      } else {
         if (!_fullNamesTable.containsKey(argumentAlias)) {
            _fullNamesTable.put(argumentAlias, argument);
         } else {
            if (_fullNamesTable.get(argumentAlias) != argument)
               throw new ArgumentDefinitionException(
                     "Registration conflict: Option  \"" + argumentAlias
                           + "\" is defined twice.");
         }
      }
   }

   // ------------------------------------------
   // Parsing - from text do data
   // ------------------------------------------

   /**
    * 
    */
   private String[] _argumentSeparators = { ":", "=" };

   private String _argumentSeparatorsRegexp = StringUtils
         .createSeparatorsRegexp(_argumentSeparators);

   private void registerNoNameArgument(ArgumentObject argument) {

      if (_remainedNoNameArguments.contains(argument) == false) {
         _remainedNoNameArguments.add(argument);
      }
   }

   private static String[] splitArgumentToNameAndValue(String inputArgument,
         String separatorsRegexp) {

      if (inputArgument.startsWith("--")) {
         String argumentWithoutPrefix = inputArgument.replaceFirst("--", "");
         return argumentWithoutPrefix.split(separatorsRegexp, 2);

      } else if (inputArgument.startsWith("-")) {
         String argumentWithoutPrefix = inputArgument.replaceFirst("-", "");
         return argumentWithoutPrefix.split(separatorsRegexp, 2);
      }
      String[] result = { inputArgument };
      return result;

   }

   private ParsedArgumentData parseInputArgument(String argument) {

      ParsedArgumentData result = new ParsedArgumentData();
      result.originalText = argument;
      String[] splitResult =
            splitArgumentToNameAndValue(argument, _argumentSeparatorsRegexp);

      if (argument.equals("--")) {
         result.meaning = ArgumentMeaning.ARGUMENT_SEPARATOR;
      } else if (argument.startsWith("--")) {
         result.meaning = ArgumentMeaning.FULL_NAME;
         result.argumentFullName =
               StringUtils.getStringFromArray(splitResult, 0);
         result.value = StringUtils.getStringFromArray(splitResult, 1);
         result.valueIsSet = (splitResult.length > 1);

      } else if (argument.startsWith("-")) {
         result.meaning = ArgumentMeaning.SHORT_NAME;
         result.argumentShortNames =
               StringUtils.getStringFromArray(splitResult, 0);
         result.value = StringUtils.getStringFromArray(splitResult, 1);
         result.valueIsSet = (splitResult.length > 1);
      } else {
         result.meaning = ArgumentMeaning.VALUE;
         result.value = argument;
         result.valueIsSet = true;
      }

      return result;

   }

   private ParsedArgumentData createValueFromArgument(String argument) {
      ParsedArgumentData result = new ParsedArgumentData();
      result.originalText = argument;
      result.meaning = ArgumentMeaning.VALUE;
      result.value = argument;
      return result;

   }

   private Deque<ParsedArgumentData> parseStringArgumentsToData(
         String[] inputArguments) {

      boolean valueSeparatorInticated = false;

      Deque<ParsedArgumentData> result = new LinkedList<ParsedArgumentData>();

      for (String inputArgument : inputArguments) {

         ParsedArgumentData parsedArgument = null;
         if (valueSeparatorInticated) {
            parsedArgument = createValueFromArgument(inputArgument);
         } else {
            parsedArgument = parseInputArgument(inputArgument);
         }

         switch (parsedArgument.meaning) {
         case ARGUMENT_SEPARATOR:
            valueSeparatorInticated = true;
            break;
         default:
            result.add(parsedArgument);
            break;
         }
      }
      return result;
   }

   // ------------------------------------------
   // ------------------------------------------
   // Parsing - main part
   // ------------------------------------------
   // ------------------------------------------

   /**
    * The main parsing method
    * @param [in] inputArguments array with arguments from the command line
    * @return ValidationResult object with the result of the validation of all arguments
    * @throws ArgumentDefinitionException
    */
   public ValidationResult parseArguments(String[] inputArguments)
         throws ArgumentDefinitionException {

      initializeParsing();

      Deque<ParsedArgumentData> remainedArguments =
            parseStringArgumentsToData(inputArguments);

      while (!remainedArguments.isEmpty()) {

         ParsedArgumentData argument = remainedArguments.poll();

         switch (argument.meaning) {
         case FULL_NAME:
            parseFullNameArgument(argument, remainedArguments);
            break;
         case SHORT_NAME:
            parseShortNameArgument(argument, remainedArguments);
            break;
         case VALUE:
            parseValueArgument(argument, remainedArguments);
            break;
         default:
            throw new ArgumentDefinitionException("Undefined argument type");
         }
      }
      return validateArguments();
   }

   private void pushArgumentValueBackToQueue(ParsedArgumentData parsedArgument,
         Deque<ParsedArgumentData> remainedArguments) {

      // create value argument and push it back to queue
      ParsedArgumentData returnedArgument = new ParsedArgumentData();
      returnedArgument.meaning = ArgumentMeaning.VALUE;
      returnedArgument.value = parsedArgument.value;
      returnedArgument.originalText = parsedArgument.value;
      returnedArgument.valueIsSet = true;
      remainedArguments.push(returnedArgument);

   }

   private void parseFullNameArgument(ParsedArgumentData parsedArgument,
         Deque<ParsedArgumentData> remainedArguments)
         throws ArgumentDefinitionException {

      if (!_fullNamesTable.containsKey(parsedArgument.argumentFullName)) {
         throw new ArgumentDefinitionException("Argument "
               + parsedArgument.argumentFullName + " has no declaration.");
      }
      ArgumentObject argument =
            _fullNamesTable.get(parsedArgument.argumentFullName);
      _enteredArguments.add(argument);

      if (parsedArgument.valueIsSet) {
         pushArgumentValueBackToQueue(parsedArgument, remainedArguments);
      }
      argument.parseArguments(remainedArguments);
   }

   private void parseShortNameArgument(ParsedArgumentData parsedArgument,
         Deque<ParsedArgumentData> remainedArguments)
         throws ArgumentDefinitionException {

      String shortNames = parsedArgument.argumentShortNames;
      int lastIndex = shortNames.length() - 1;

      for (int shortNameIndex = 0; shortNameIndex < shortNames.length(); shortNameIndex++) {
         Character shortName = shortNames.charAt(shortNameIndex);

         if (!_shortNamesTable.containsKey(shortName)) {
            throw new ArgumentDefinitionException("Argument " + shortName
                  + " has no declaration.");
         }

         ArgumentObject argument = _shortNamesTable.get(shortName);
         _enteredArguments.add(argument);

         // if value is set to short names, it will be assigned to last
         // argument
         if ((shortNameIndex == lastIndex)) {
            if (parsedArgument.valueIsSet) {
               pushArgumentValueBackToQueue(parsedArgument, remainedArguments);
            }
            argument.parseArguments(remainedArguments);
         } else {
            Deque<ParsedArgumentData> emptyQueue =
                  new LinkedList<ParsedArgumentData>();
            argument.parseArguments(emptyQueue);
         }

      }

   }

   private void parseValueArgument(ParsedArgumentData parsedArgument,
         Deque<ParsedArgumentData> remainedArguments)
         throws ArgumentDefinitionException {

      ArgumentObject argument = getUndefinedArgument(_remainedNoNameArguments);
      if (argument == null) {
         throw new ArgumentDefinitionException("Undefined argument: "
               + parsedArgument.value);
      }

      // push back value, because it is not name of argument
      pushArgumentValueBackToQueue(parsedArgument, remainedArguments);

      argument.parseArguments(remainedArguments);
   }

   private ArgumentObject getUndefinedArgument(
         Queue<ArgumentObject> remainedArguments) {

      while (!_remainedNoNameArguments.isEmpty()) {

         ArgumentObject argument = _remainedNoNameArguments.poll();
         if (argument != null && argument.isEntered() == false) {
            return argument;
         }
      }
      return null;
   }

   // ------------------------------------------
   // Validation
   // ------------------------------------------

   private ValidationResult validateArguments() {

      ValidationResult firstFail = null;
      for (ArgumentObject argument : _allDefinedArguments.getAllArguments()) {
         ValidationResult validationResult = argument.validateArgument();
         if (validationResult.isValid() == false && firstFail == null) {
            firstFail = validationResult;
         }
      }
      if (firstFail != null) {
         return firstFail;
      }
      return ValidationResult.createValidResponse();
   }

   // ------------------------------------------
   // Parsing result
   // ------------------------------------------

   /**
    * 
    * @return the queue with all entered arguments
    */
   public Queue<ArgumentObject> enteredArguments() {
      Queue<ArgumentObject> result =
            new LinkedList<ArgumentObject>(_enteredArguments);

      return result;
   }

}
